package httproute

import (
	"context"

	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/sets"
	gwv1 "sigs.k8s.io/gateway-api/apis/v1"

	"github.com/kgateway-dev/kgateway/v2/pkg/kgateway/query"
	"github.com/kgateway-dev/kgateway/v2/pkg/kgateway/translator/metrics"
	"github.com/kgateway-dev/kgateway/v2/pkg/kgateway/wellknown"
	"github.com/kgateway-dev/kgateway/v2/pkg/logging"
	"github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/ir"
	reports "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/reporter"
)

var logger = logging.New("translator/httproute")

// TODO: Uncomment when the gateway_http_route_translator_test.go is uncommented.
// var (
// 	awsMissingFuncRefError                = eris.New("upstreams must have a logical name specified in the backend ref via the parameters extensionref")
// 	azureMissingFuncRefError              = eris.New("upstreams must have a function name specified in the backend ref via the parameters extensionref")
// 	nonFunctionUpstreamWithParameterError = eris.New("parameters extensionref is only supported for aws and azure upstreams")
// )

func TranslateGatewayHTTPRouteRules(
	ctx context.Context,
	routeInfo *query.RouteInfo,
	reporter reports.ParentRefReporter,
	baseReporter reports.Reporter,
) []ir.HttpRouteRuleMatchIR {
	var finalRoutes []ir.HttpRouteRuleMatchIR
	routesVisited := sets.New[types.NamespacedName]()

	// Only HTTPRoute types should be translated.
	_, ok := routeInfo.Object.(*ir.HttpRouteIR)
	if !ok {
		return finalRoutes
	}

	translateGatewayHTTPRouteRulesUtil(
		ctx, routeInfo, reporter, baseReporter, &finalRoutes, routesVisited, nil)
	return finalRoutes
}

// translateGatewayHTTPRouteRulesUtil is a helper to translate an HTTPRoute.
// In case of route delegation, this function is recursively invoked to flatten the delegated route tree.
func translateGatewayHTTPRouteRulesUtil(
	ctx context.Context,
	routeInfo *query.RouteInfo,
	reporter reports.ParentRefReporter,
	baseReporter reports.Reporter,
	outputs *[]ir.HttpRouteRuleMatchIR,
	routesVisited sets.Set[types.NamespacedName],
	delegatingParent *ir.HttpRouteRuleMatchIR,
) {
	// Only HTTPRoute types should be translated.
	route, ok := routeInfo.Object.(*ir.HttpRouteIR)
	if !ok {
		return
	}

	// This function is called multiple times during translation of resources, and it is
	// only required to start the resource metrics tracking when the parent is a Gateway.
	if routeInfo.ParentRef.Kind != nil && *routeInfo.ParentRef.Kind == wellknown.GatewayKind {
		defer (metrics.CollectTranslationMetrics(metrics.TranslatorMetricLabels{
			Name:       string(routeInfo.ParentRef.Name),
			Namespace:  routeInfo.GetNamespace(),
			Translator: "TranslateHTTPRoute",
		}))(nil)
	}

	for ruleIdx, rule := range route.Rules {
		if len(rule.Matches) == 0 {
			// from the spec:
			// If no matches are specified, the default is a prefix path match on “/”, which has the effect of matching every HTTP request.
			rule.Matches = []gwv1.HTTPRouteMatch{{}}
		}

		outputRoutes := translateGatewayHTTPRouteRule(
			ctx,
			routeInfo,
			route,
			rule,
			ruleIdx,
			reporter,
			baseReporter,
			outputs,
			routesVisited,
			delegatingParent,
		)
		for _, outputRoute := range outputRoutes {
			// The above function will return a nil route if a matcher fails to apply plugins
			// properly. This is a signal to the caller that the route should be dropped.
			//		if outputRoute == nil {
			//			continue
			//		}

			*outputs = append(*outputs, outputRoute)
		}
	}
}

func translateGatewayHTTPRouteRule(
	ctx context.Context,
	gwroute *query.RouteInfo,
	parent *ir.HttpRouteIR,
	rule ir.HttpRouteRuleIR,
	ruleIdx int,
	reporter reports.ParentRefReporter,
	baseReporter reports.Reporter,
	outputs *[]ir.HttpRouteRuleMatchIR,
	routesVisited sets.Set[types.NamespacedName],
	delegatingParent *ir.HttpRouteRuleMatchIR,
) []ir.HttpRouteRuleMatchIR {
	routes := make([]ir.HttpRouteRuleMatchIR, 0, len(rule.Matches))

	for idx, match := range rule.Matches {
		// HTTPRoute names are being introduced to upstream as part of https://github.com/kubernetes-sigs/gateway-api/issues/995
		// For now, the HTTPRoute needs a unique name for each Route to support features that require the route name
		// set (basic ratelimit, route-level jwt, etc.). The unique name is generated by appending the index of the route to the
		// HTTPRoute name.namespace.
		uniqueRouteName := gwroute.UniqueRouteName(ruleIdx, idx, rule.Name)

		outputRoute := ir.HttpRouteRuleMatchIR{
			ExtensionRefs:        rule.ExtensionRefs,
			AttachedPolicies:     rule.AttachedPolicies,
			Parent:               parent,
			ListenerParentRef:    gwroute.ListenerParentRef,
			ParentRef:            gwroute.ParentRef,
			Name:                 uniqueRouteName,
			Backends:             nil,
			MatchIndex:           idx,
			Match:                match,
			DelegatingParent:     delegatingParent,
			PrecedenceWeight:     parent.PrecedenceWeight,
			RouteAcceptanceError: rule.Err,
		}

		if len(rule.Backends) > 0 {
			setRouteAction(
				ctx,
				gwroute,
				rule,
				&outputRoute,
				reporter,
				baseReporter,
				match,
				outputs,
				routesVisited,
			)
		}

		// TODO: this is not true; plugins can add actions later.
		// It is possible for a parent route to not produce an output route action
		// if it only delegates and does not directly route to a backend.
		// We should only set a direct response action when there is no output action
		// for a parent rule and when there are no delegated routes because this would
		// otherwise result in a top level matcher with a direct response action for the
		// path that the parent is delegating for.

		// A parent route that delegates to a child route should not have an output route
		// action (outputRoute.Action) as the routes are derived from the child route.
		// So this conditional ensures that we do not create a top level route matcher
		// for the parent route when it delegates to a child route.

		// TODO: need to be sure we can remove this if, and that we handle it later. routes with now backends might still have extensions
		// on them that make them useful, but we processes these later
		// if len(outputRoute.Backends) > 0 {
		routes = append(routes, outputRoute)
		// }
	}
	return routes
}

func setRouteAction(
	ctx context.Context,
	gwroute *query.RouteInfo,
	rule ir.HttpRouteRuleIR,
	outputRoute *ir.HttpRouteRuleMatchIR,
	reporter reports.ParentRefReporter,
	baseReporter reports.Reporter,
	match gwv1.HTTPRouteMatch,
	outputs *[]ir.HttpRouteRuleMatchIR,
	routesVisited sets.Set[types.NamespacedName],
) {
	backends := rule.Backends
	for _, backend := range backends {
		// If the backend is an HTTPRoute, it implies route delegation
		// for which delegated routes are recursively flattened and translated
		if backend.Delegate != nil {
			outputRoute.Delegates = true
			// Flatten delegated HTTPRoute references
			err := flattenDelegatedRoutes(
				ctx,
				gwroute,
				backend,
				reporter,
				baseReporter,
				match,
				outputs,
				routesVisited,
				outputRoute,
			)
			if err != nil {
				query.ProcessBackendError(err, reporter)
				outputRoute.RouteReplacementError = err
			}
			continue
		}

		if err := backend.Backend.Err; err != nil {
			query.ProcessBackendError(err, reporter)
			logger.Debug("error on backend upstream", "error", err)
		}

		httpBackend := ir.HttpBackend{
			Backend:          *backend.Backend, // TODO: Nil check?
			AttachedPolicies: backend.AttachedPolicies,
		}
		outputRoute.Backends = append(outputRoute.Backends, httpBackend)
	}
}

/* TODO: demonstrate that we can replace this with 'virtual' GKs
func applyBackendPlugins(
	obj client.Object,
	backendRef gwv1.BackendObjectReference,
	plugins registry.PluginRegistry,
) (*v1.Destination, bool) {
	for _, bp := range plugins.GetBackendPlugins() {
		if dest, ok := bp.ApplyBackendPlugin(obj, backendRef); ok {
			return dest, true
		}
	}
	return nil, false
}
*/
